/*
 * Copyright (c) 2010, Brookhaven National Laboratory
 * All rights reserved.
 * The software in this package is published under the terms
 * of the BSD license a copy of which has been included with
 * this distribution in the license.txt file.
 */

package org.oxtypes.protobuf;

import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;

import org.omg.dds.type.dynamic.DynamicType;
import org.omg.dds.type.dynamic.MemberDescriptor;
import org.oxtypes.DynamicData;

import com.google.protobuf.WireFormat.*;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.Descriptors.*;

// OxTypes

import com.google.protobuf.PublicWireFormat;
import org.oxtypes.protobuf.impl.LengthDelimitedField;
import org.oxtypes.protobuf.impl.PackedFieldWriter;
import org.oxtypes.protobuf.impl.RepeatedFieldWriter;

/**
 *
 */
public class DynamicDataWriter {

    public DynamicDataWriter(){
       packedWriter = new PackedFieldWriter();
       repeatedWriter = new RepeatedFieldWriter();
    }

    /** Calculates a byte size of sample */
    public int computeByteSize(DynamicData sample){
       rootField = new CompositeField();
       computeMessageSize(sample, rootField);
       return rootField.getSize();
    }

    /** Serializes sample and writes it into array of bytes */
    public void write(DynamicData sample, byte[] bytes) throws IOException {

        if(rootField == null) computeByteSize(sample);

        CodedOutputStream output = CodedOutputStream.newInstance(bytes);
        writeMessage(sample, rootField, output);
    }

    private void computeMessageSize(DynamicData dd, LengthDelimitedField ldField){

        DynamicTypeRegistry dtr = DynamicTypeRegistry.getInstance();

        DynamicType dt = dd.getType();
        Descriptor d = dtr.getDescriptor(dt);

        for(MemberDescriptor md :  dd.getDescriptors()){
            FieldDescriptor fd = d.findFieldByNumber(md.getId());
            computeFieldSize(dd, fd, ldField);
        }
    }

    private void writeMessage(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output)
      throws IOException {

        DynamicTypeRegistry dtr = DynamicTypeRegistry.getInstance();

        DynamicType dt = dd.getType();
        Descriptor d = dtr.getDescriptor(dt);

        for(MemberDescriptor md :  dd.getDescriptors()){
            FieldDescriptor fd = d.findFieldByNumber(md.getId());
            writeField(dd, fd, ldField, output);
        }
    }

    private void computeFieldSize(DynamicData dd,
            FieldDescriptor fd, LengthDelimitedField ldField){
        if(!fd.isRepeated()) computeElementSize(dd, fd, ldField);
        else if(fd.isPacked()) packedWriter.computeByteSize(dd, fd, ldField);
        else repeatedWriter.computeByteSize(dd, fd, ldField);
    }

    private void writeField(DynamicData dd, FieldDescriptor fd,
            LengthDelimitedField ldField, CodedOutputStream output) throws IOException {
        if(!fd.isRepeated()) writeElement(dd, fd, ldField, output);
        else if(fd.isPacked()) packedWriter.write(dd, fd, ldField, output);
        else repeatedWriter.write(dd, fd, ldField, output);
    }
 
    private void computeElementSize(DynamicData dd,
            FieldDescriptor fd, LengthDelimitedField ldField){

       int byteSize = CodedOutputStream.computeTagSize(fd.getNumber());
       if(fd.getLiteType() == FieldType.GROUP){
           byteSize *= 2;
       }
       ldField.addSize(byteSize);

       computeElementSizeNoTag(dd, fd, ldField);      
    }

    private void writeElement(DynamicData dd, FieldDescriptor fd,
            LengthDelimitedField ldField, CodedOutputStream output) throws IOException {
       // write tag
       output.writeTag(fd.getNumber(), fd.getLiteType().getWireType());

       // write element
       writeElementNoTag(dd, fd, ldField, output);
    }

    /** Copied from FieldSet */
    private void computeElementSizeNoTag(DynamicData dd,
        FieldDescriptor fd, LengthDelimitedField ldField){

        int id = fd.getNumber();

     switch(fd.getLiteType()){
        // fixed length types
        case DOUBLE  : ldField.addSize(doubleSize); break;
        case FLOAT   : ldField.addSize(floatSize); break;
        case FIXED64 : ldField.addSize(fixed64Size); break;
        case FIXED32 : ldField.addSize(fixed32Size); break;
        case SFIXED64: ldField.addSize(sfixed64Size); break;
        case SFIXED32: ldField.addSize(sfixed32Size); break;
        // variable length types (varints)
        case INT64   : ldField.addSize(
            CodedOutputStream.computeInt64SizeNoTag(dd.getInt64Value(id))); break;
        case INT32   : ldField.addSize(
            CodedOutputStream.computeInt32SizeNoTag(dd.getInt32Value(id))); break;
        case UINT64  : ldField.addSize(
            CodedOutputStream.computeUInt64SizeNoTag(dd.getInt64Value(id))); break;
        case UINT32  : ldField.addSize(
            CodedOutputStream.computeUInt32SizeNoTag(dd.getInt32Value(id))); break;
        case SINT64  : ldField.addSize(
            CodedOutputStream.computeSInt64SizeNoTag(dd.getInt64Value(id))); break;
        case SINT32  : ldField.addSize(
            CodedOutputStream.computeSInt32SizeNoTag(dd.getInt32Value(id))); break;
        case BOOL    : ldField.addSize(
            CodedOutputStream.computeBoolSizeNoTag(dd.getBooleanValue(id))); break;
        case ENUM    : ldField.addSize(
            CodedOutputStream.computeEnumSizeNoTag(dd.getInt32Value(id))); break;
        case GROUP   : computeMessageSize(dd.getComplexValue(id), ldField); break;
        // length-delimited types
        case STRING  : ldField.addSize(
            CodedOutputStream.computeStringSizeNoTag(dd.getStringValue(id)));
        case BYTES   : computeBytesSizeNoTag(dd, id, ldField); break;
        case MESSAGE : computeEmbeddedMessageSize(dd, id, ldField); break;
     }

    }

    private void writeElementNoTag(DynamicData dd, FieldDescriptor fd, 
        LengthDelimitedField ldField, CodedOutputStream output) throws IOException {

      int id = fd.getNumber();

      switch(fd.getLiteType()){
        // fixed length types
        case DOUBLE  : output.writeDoubleNoTag(dd.getFloat64Value(id)); break;
        case FLOAT   : output.writeFloatNoTag(dd.getFloat32Value(id)); break;
        case FIXED64 : output.writeFixed64NoTag(dd.getInt64Value(id)); break;
        case FIXED32 : output.writeFixed32NoTag(dd.getInt32Value(id)); break;
        case SFIXED64: output.writeSFixed64NoTag(dd.getInt64Value(id)); break;
        case SFIXED32: output.writeSFixed32NoTag(dd.getInt32Value(id)); break;
        // variable length types
        case INT64   : output.writeInt64NoTag(dd.getInt64Value(id)); break;
        case INT32   : output.writeInt32NoTag(dd.getInt32Value(id)); break;
        case UINT64  : output.writeUInt64NoTag(dd.getInt64Value(id)); break;
        case UINT32  : output.writeUInt32NoTag(dd.getInt32Value(id)); break;
        case SINT64  : output.writeSInt64NoTag(dd.getInt64Value(id)); break;
        case SINT32  : output.writeSInt32NoTag(dd.getInt32Value(id)); break;
        case BOOL    : output.writeBoolNoTag(dd.getBooleanValue(id)); break;
        case ENUM    : output.writeEnumNoTag(dd.getInt32Value(id)); break;
        case GROUP   :
          writeMessage(dd.getComplexValue(id), ldField, output);
          output.writeTag(id, PublicWireFormat.WIRETYPE_END_GROUP);
          break;
        // length-delimited types
        case STRING  : output.writeStringNoTag(dd.getStringValue(id)); break;
        case BYTES   : writeBytesNoTag(dd, id, output); break;
        case MESSAGE : writeEmbeddedMessage (dd, id, ldField, output); break;

     }
    }

    private void computeBytesSizeNoTag(DynamicData value, int id,
            LengthDelimitedField ldField) {
        int length = value.getComplexValue(id).getLength();
        ldField.addSize(CodedOutputStream.computeRawVarint32Size(length) + length);
    }

    private void writeBytesNoTag(DynamicData value, int id,
            CodedOutputStream output) throws IOException {
       throw new java.lang.UnsupportedOperationException();
    }

    private void computeEmbeddedMessageSize(DynamicData dd, int id,
            LengthDelimitedField ldField) {
        throw new java.lang.UnsupportedOperationException();
    }

    private void writeEmbeddedMessage(DynamicData value, int id,
            LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {
       throw new java.lang.UnsupportedOperationException();
    }


    public class CompositeField implements LengthDelimitedField {

        int byteSize;
        Map<Integer, LengthDelimitedField> fields;
        
        public CompositeField() {
            fields = new TreeMap<Integer, LengthDelimitedField>();
        }

        public int getSize(){
            return byteSize;
        }

        public void addSize(int size){
            byteSize += size;
        }

        public void addField(int id, LengthDelimitedField ldField){
            fields.put(id, ldField);
            byteSize += ldField.getSize();
        }

        public LengthDelimitedField getField(int id){
            return fields.get(id);
        }
    } 

    private LengthDelimitedField rootField;

    private PackedFieldWriter packedWriter;
    private RepeatedFieldWriter repeatedWriter;


    static private int doubleSize = CodedOutputStream.computeDoubleSizeNoTag(0.0);
    static private int floatSize = CodedOutputStream.computeFloatSizeNoTag(0);
    static private int fixed64Size = CodedOutputStream.computeFixed64SizeNoTag(0);
    static private int fixed32Size = CodedOutputStream.computeFixed32SizeNoTag(0);
    static private int sfixed64Size = CodedOutputStream.computeSFixed64SizeNoTag(0);
    static private int sfixed32Size = CodedOutputStream.computeSFixed32SizeNoTag(0);

}
